Hi大家好,
這是我參加 iT 邦幫忙鐵人賽的第 1 次挑戰,這次的主題聚焦在結合 Python 爬蟲、RAG(檢索增強生成)與 AI,打造一套 PTT 文章智慧問答系統。在過程中,我會依照每天進度上傳程式碼到 GitHub ,方便大家參考學習。也歡迎留言或來信討論,我的信箱是 gerryearth@gmail.com。
我們今天將開始實作第一個 API——文章列表查詢 API,並加入條件篩選、分頁、以及 API 文件生成,讓系統能夠對外穩定提供資料查詢服務,也為後續的智慧問答功能打好基礎。
Article
模型的 Serializer 與 ViewGET /api/posts/
查詢 APIpip install djangorestframework
在 settings.py
中加入:
INSTALLED_APPS = [
...
'rest_framework',
]
有了這個模組,我們就能在
views.py
建立API
,例如以下範例可以接受用戶端 GET 請求(request),並給予回應(response):from rest_framework.views import APIView from rest_framework.response import Response class HelloView(APIView): def get(self, request): return Response({"message": "Hello, world!"})
請先安裝所需套件:
pip install django-filter drf-spectacular
django-filter
在 API 或 Django views 中,讓你可以用查詢參數 (query parameters) 進行資料過濾。drf-spectacular
可以自動產生 OpenAPI 3 格式的 API 規格文件,並且能提供互動式的 Swagger / Redoc 頁面。
並在 settings.py
加上:
INSTALLED_APPS = [
'django.contrib.admin',
...
'django_filters',
'drf_spectacular',
]
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
在 ptt_rag_dev/urls.py
建立:
from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
urlpatterns = [
path('api/admin/', admin.site.urls),
path('api/', include('article.urls')),
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/schema/doc/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]
並建立 article/urls.py
:
from django.urls import path
from . import views
urlpatterns = [
path('posts/', views.ArticleListView.as_view(), name='article-list'),
]
ArticleSerializer
在 article/serializers.py
中新增:
from rest_framework import serializers
from .models import Article
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'
class ArticleListRequestSerializer(serializers.Serializer):
author_name = serializers.CharField(help_text="作者名稱", write_only=True, required=False)
board_name = serializers.CharField(help_text="看板名稱", write_only=True, required=False)
start_date = serializers.DateField(help_text="起始日期", write_only=True, required=False)
end_date = serializers.DateField(help_text="結束日期", write_only=True, required=False)
limit = serializers.IntegerField(help_text="每頁返回的筆數 (預設 50)", write_only=True, default=50, min_value=1)
offset = serializers.IntegerField(help_text="從第幾筆開始 (預設 0)", write_only=True, required=False, min_value=0)
serializers
可以使 Django 的資料模型(如:Article)透過 API 傳給前端時,主要的功能如下:
ArticleSerializer
的目的是將Article
模型序列化 + 反序列化(含驗證):用於資料模型操作(資料輸入輸出),而ArticleListRequestSerializer
只用來反序列化 + 驗證輸入資料(不序列化、不儲存),可以用來過濾資料、搜尋條件或分頁。
article/views.py
from datetime import datetime, time
from rest_framework import status, serializers
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.pagination import LimitOffsetPagination
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse, inline_serializer
from .models import Article
from .serializers import ArticleSerializer, ArticleListRequestSerializer
import traceback
from log_app.models import Log
class ArticleListView(APIView):
@extend_schema(
description="取得最新 50 篇文章,可使用 limit、offset 進行分頁,可使用作者名稱、版面、時間範圍進行過濾。",
parameters=[
OpenApiParameter("limit", int, OpenApiParameter.QUERY, description="每頁返回的筆數 (預設 50)"),
OpenApiParameter("offset", int, OpenApiParameter.QUERY, description="從第幾筆開始 (預設 0)"),
OpenApiParameter("author_name", str, OpenApiParameter.QUERY, description="篩選特定發文者的文章"),
OpenApiParameter("board_name", str, OpenApiParameter.QUERY, description="篩選特定版面的文章"),
OpenApiParameter("start_date", str, OpenApiParameter.QUERY, description="篩選起始日期 (YYYY-MM-DD)", ),
OpenApiParameter("end_date", str, OpenApiParameter.QUERY, description="篩選結束日期 (YYYY-MM-DD)", ),
],
responses={
200: OpenApiResponse(
response=inline_serializer(
name='ArticleListResponse',
fields={
'count': serializers.IntegerField(read_only=True),
'next': serializers.CharField(read_only=True),
'previous': serializers.CharField(read_only=True),
'results': ArticleSerializer(many=True, read_only=True),
}
),
)
},
)
def get(self, request):
article_list_request_serializer = ArticleListRequestSerializer(data=request.query_params)
if not article_list_request_serializer.is_valid():
Log.objects.create(level='ERROR', category='user-posts', message='查詢參數不合法',
traceback=traceback.format_exc())
return Response(article_list_request_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
articles = Article.objects.all()
author_name = article_list_request_serializer.validated_data.get("author_name")
board_name = article_list_request_serializer.validated_data.get("board_name")
start_date = article_list_request_serializer.validated_data.get("start_date")
end_date = article_list_request_serializer.validated_data.get("end_date")
if author_name:
articles = articles.filter(author=author_name)
if board_name:
articles = articles.filter(board=board_name)
start_datetime = datetime.combine(start_date, time.min) if start_date else None
end_datetime = datetime.combine(end_date, time.max) if end_date else None
if start_datetime and end_datetime:
articles = articles.filter(post_time__range=[start_datetime, end_datetime])
elif start_datetime:
articles = articles.filter(post_time__gte=start_datetime)
elif end_datetime:
articles = articles.filter(post_time__lte=end_datetime)
paginator = LimitOffsetPagination()
paginator.default_limit = 50
paginated_queryset = paginator.paginate_queryset(articles.order_by('id'), request)
serializer = ArticleSerializer(paginated_queryset, many=True)
return paginator.get_paginated_response(serializer.data)
以下簡單說明此 API 的重點功能:
drf-spectacular
提供,用來自動產生 API 文件(Swagger UI)。limit
和 offset
控制查詢結果。ArticleListView.get()
:提供 GET 請求,用來查詢文章列表,具備以下特性:
limit
:一次取回幾筆資料(預設 50)。offset
:從哪一筆資料開始(預設 0)。author_name
:依發文者名稱過濾。board_name
:依版面名稱過濾。start_date
, end_date
:依發文時間範圍過濾(格式 YYYY-MM-DD)。使用 ArticleListRequestSerializer
驗證查詢參數是否符合規格。
根據提供的條件(作者、版面、時間區間)過濾資料庫的 Article
物件。
start_date
對應 00:00:00end_date
對應 23:59:59datetime.combine()
建立完整的 datetime
物件。使用 LimitOffsetPagination
回傳有分頁資訊的結果(count
, next
, previous
, results
)。
若查詢參數格式錯誤,會紀錄錯誤 Log 並回傳 400 回應。
{
"count": 0,
"next": "string",
"previous": "string",
"results": [
{
"id": 0,
"board": "string",
"title": "string",
"author": "string",
"content": "string",
"post_time": "2025-06-23T08:53:58.640Z",
"url": "string"
}
]
}
都完成後記得測試 API 喔!
API 完成的結果會類似以下範例:
明天【Day15】建立文章 API:ID查詢與統計
我們將會練習建立另外兩個API:
/api/posts/{id}/:查詢特定文章詳細內容。
/api/statistics/:查詢統計資訊(文章總數),可透過 時間範圍、版面、作者 過濾。